昨天做完 graceful shutdown 之後。今天會來講述,如何把 nestjs 的應用作容器化。
應用程式容器化,是把應用程式的運行環境也透過標準化的方式打包起來。不同於以往針對每個應用程式還需要到執行的伺服器設定運行環境。簡化了部署的難度。
Docker 提供了一個標準的打包文件格式,可以把運行環境連同程式打包成映像檔案。透過推送到映像檔儲存庫,伺服器只要具備 Docker 運行環境,就可以從映像檔儲存庫拉取最新的應用程式映像檔案來執行。這種可運行的檔案,一般會被稱作 artifact 。一般會需要根據針對 artifact 作版本控制,來確認部屬的應用程式具有哪些行為。
FROM node:20.10.0-alpine as build
RUN mkdir /app
WORKDIR /app
RUN npm i -g pnpm
COPY src nest-cli.json package.json pnpm-lock.yaml tsconfig.build.json tsconfig.json /app/
RUN pnpm install --frozen-lockfile && pnpm run build
FROM node:20.10.0-alpine as prod
RUN mkdir /app
WORKDIR /app
RUN npm i -g pnpm
COPY --from=build /app/dist /app/dist
COPY --from=build /app/package.json /app/pnpm-lock.yaml /app/
COPY --from=build /app/lua /app/dist/lua
RUN pnpm install --production && npm uninstall -g pnpm
USER node
ENTRYPOINT [ "node", "./dist/main" ]
Dockerfile 主要會撰寫幾個重要的屬性:
上面範例,使用的是 Multi Stage build 。目標是為了,讓 build tool 不會影響到真正執行的 image size 。概念上是,每一行在 Dockefile 上的命令都會變成一個 commit 在 image 上。
在 docker compose 上,可以透過 target 指定要作哪個 stage 的 build 如下
ticket-mn-api:
container_name: ticket-mn-api
build:
context: .
dockerfile: ./Dockerfile
target: prod
image: ticket-mn-api
ports:
- 3000:3000
透過 docker history 指令,可以查看每個 docker commit 的大小:
name: Image build
on:
push:
branches:
- master
tags:
- v*
pull_request:
env:
IMAGE_NAME: ticket-mn-api
JWT_ACCESS_TOKEN_SECRET: ${{ secrets.JWT_ACCESS_TOKEN_SECRET }}
JWT_ACCESS_TOKEN_EXPIRATION_MS: 10000
JWT_REFRESH_TOKEN_SECRET: ${{ secrets.JWT_REFRESH_TOKEN_SECRET }}
JWT_REFRESH_TOKEN_EXPIRATION_MS: 36000
NODE_ENV: dev
REDIS_URL: ${{ secrets.REDIS_URL }}
DB_URI: ${{ secrets.DB_URI }}
GIN_MODE: release
jobs:
push:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker compose build ticket-mn-api
- name: Tag with Image
run: docker tag ticket-mn-api:latest $IMAGE_NAME:$GITHUB_SHA
- name: Log in to registry
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Push to registry
run: |
IMAGE_ID=ghcr.io/nodejs-typescript-classroom/$IMAGE_NAME
IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
[ "$VERSION" == "main" ] && VERSION=latest
echo IMAGE_ID=$IMAGE_ID
echo VERSION=$VERSION
docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
docker push $IMAGE_ID:$VERSION
以上設定是透過 docker compose build 來建立 image。然後把 image 送到對應的 github organization 的 package
這樣一旦 commit 到特定的 repository 或是下 tag v 就會啟動 build image 並且 push 到 image registry 如下
name: Node
on:
push:
branches:
- master
pull_request:
branches:
- master
env:
NODE_ENV: dev
JWT_ACCESS_TOKEN_SECRET: ${{ secrets.JWT_ACCESS_TOKEN_SECRET }}
JWT_ACCESS_TOKEN_EXPIRATION_MS: ${{ secrets.JWT_ACCESS_TOKEN_EXPIRATION_MS }}
JWT_REFRESH_TOKEN_SECRET: ${{ secrets.JWT_REFRESH_TOKEN_SECRET }}
JWT_REFRESH_TOKEN_EXPIRATION_MS: ${{ secrets.JWT_REFRESH_TOKEN_EXPIRATION_MS }}
DB_URI: ${{ secrets.DB_URI }}
jobs:
cache-and-install:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 9
run_install: false
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 20.10.0
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run Test
run: pnpm test
- name: Run Test
run: pnpm test:e2e
每次 commit 也都會有測試驗證,如下圖
docker start ticket-mn-api
docker stop ticket-mn-api
docker logs -f ticket-mn-api
前面有說過, Dockerfile 可以撰寫 health_check 屬性來決定當下 service 的狀態。這個屬性在服務間有啟動的相依性時,就可以發揮作用。
假設是透過 docker compose 的文件去撰寫,就可以使用 depends_on 條件去作處理。例如: ticket-mn-api 需要 db 跟 redis 狀態為 healthy 才啟動就可以撰寫如下:
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
雖然本篇是 nodejs 的 Port Mapping 沒有作修改是採用原本的 3000。然而,透過像是容器編排服務比如像是 k8s 的 Service 或是 AWS Fargate 等等本身就能作到服務阜口切換。這是可以讓撰寫程式與部署狀態不會相互被影響的技術。
當然還是可能會遇到一些說 N 年經驗的 "Devops" 職銜的人,跟開發者說你的 Port 要修改,他才能部屬。這類人也許真的處在跟我們一般人是不相同的異世界。這就不在本文所撰寫的相同時空不在討論之列。
身為開發者,還是必須要能夠掌握一些關於系統部屬的技術。這樣在程式真正載運行時,才能對一些正式環境發生的事件,有掌握度來分析 bug 事件。